在前一篇文章我們知道 suspend 函式必須要在 Coroutine scope 裏面才能執行,本篇文章我們來了解一下兩個 Coroutine Builder : launch()
、 async()
如果一個 Coroutine 的回傳值沒有回傳值,也就是回傳 Unit
時,就可以使用 launch()
來建立一個 Coroutine。
使用範例:
fun main() = runBlocking {
launch {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
我們發現,這段程式碼會先執行最外層的 println("Done")
,接著才是 launch()
裏面的函式。
這是因為當使用 launch()
時,會建立一個新的 Coroutine,成為 runBlocking()
的子 Coroutine。如果在使用 launch()
時,沒有帶入 Coroutine context ,那麼預設會使用的調度器為 Dispatchers.Default ,也就是背景運算的 coroutine。所以就會出現 println("Done")
先執行的情況。
launch()
改為 coroutineScope()
會發生什麼事呢?fun main() = runBlocking {
coroutineScope {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
→ 結果則是按照原本的順序來執行。因為利用 coroutineScope()
並不會建立新的 coroutine 而是繼承外層的 coroutine context,也就是說,在 coroutineScope()
裏面與外面的 println("Done")
其實都是在同一個 coroutine,所以會按照其順序來執行。
在 launch()
裡有三行程式,除了 delay(500)
是 suspend 函式以外,其他兩行程式都是一般的程式碼,所以我們可以將 launch()
內部的程式碼重構,把這裏面的程式碼抽取出去。
suspend fun launchFun() {
println("launch Start")
delay(500)
println("launch End")
}
因為這三行程式碼有包含一行 suspend 函式 - delay()
,所以這個函式必須要加上 suspend
來修飾。
suspend 函式只能在 CoroutineScope 或是被另一個 suspend 函式調用。
launch()
的回傳值是 Job
, Job
是代表一個可被取消的任務,我們可以呼叫 cancel()
取消該 coroutine 的執行。
如下:
fun main() = runBlocking {
val job = launch {
repeat(100_000) { index ->
println("launch Start $index")
delay(500)
println("launch End $index")
}
}
delay(1100)
job.cancel()
println("Done")
}
→ 在上面的範例中,我們在 launch
中執行了一段會執行十萬次的任務,這個任務首先先印出 launch Start $index
,接著暫停該 Coroutine 500 毫秒,在 500 毫秒暫停時間結束之後,就會列印出 launch End $index
。
→ 我們把 launch 的回傳值儲存在 job
變數上,在外層的 coroutine 執行 1100 毫秒之後,就呼叫 job.cancel()
把十萬次的任務停止。
在這個範例中,Coroutine 的執行區塊可以分成兩塊,紅色區塊我稱為 「Coroutine1」,粉紫色區塊我稱為「Coroutine2」。我們可以看一下它執行的時間軸。
Coroutine1 因為調用 delay(1100)
,所以 Coroutine1 暫停 1100 毫秒,在暫停之後調用了 Coroutine2 的 cancel()
,所以 Coroutine2 的任務被取消,當 Coroutine2 的任務被取消之後, Coroutine1 就能繼續被執行。
在前面的範例中,我們在 runBlocking
中建立一個 launch
,在 launch
所產生的 coroutine 就是在 runBlocking
裏面的子 coroutine,所以當我們執行時,預設是會先執行外層的 coroutine,接著才是內層的 coroutine 。
所以下面的範例會先執行 println("Done")
,接著才會執行 launch
內部的任務。
fun main() = runBlocking {
launch {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
假如我們希望能夠先完成 launch
裏面的任務,完成之後我們才接續執行下面的任務,我們可以使用 job.join()
。
fun main() = runBlocking {
val job = launch {
println("launch Start")
delay(500)
println("launch End")
}
job.join()
println("Done")
}
在 Kotlin 的 Coroutine 中,提供了 joinAll()
來同時針對多個 Job 來呼叫其 join()
。
fun main() = runBlocking {
val job = launch {
println("launch Start")
delay(500)
println("launch End")
}
joinAll(job)
println("Done")
}
其實 joinAll()
只是呼叫帶入 Job 的 join()
。
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
如同 launch()
, async()
也是用來建立 Coroutine 的,只不過與 launch()
不同的是, async()
是用來處理有回傳值的非同步任務,而且它回傳的是 Deferred
而不是 Job
。
fun main() = runBlocking{
val deferred = async {
println("async start")
delay(500)
println("async end")
50
}
val deferredValue = deferred.await()
println("done $deferredValue")
}
我們將 async
回傳的值存到變數 deferred
裏面,這時候 async
裏面的任務還沒有開始執行,等到呼叫 await()
之後,就會執行 async
,並且在這邊等待 async
完成,所以這邊回傳的值就會是 async
區塊中的回傳值,如上方範例的 50。
在 Kotlin 中,lambda 的最後一行就是它的回傳值,我們可以省略
return
,不過如果要使用return
也可以。
return@async 50
await()
的使用類似 join()
。前面有提到, join()
是會把該 Coroutine 任務完成之後,才會繼續往下走, await()
的用法也是一樣,當呼叫 await()
的時候,該 Coroutine 就會開始執行,直到結束或是發生例外。與 join()
不同的是, await()
是會回傳在 async
區塊的回傳值,如上方的 50。
值得注意的是,我們在 async
並沒有宣告回傳的資料型別, Kotlin 會自動做型別推斷。當然我們也可以自行加上型別。如下:
async<Int> {
println("async start")
delay(500)
println("async end")
50
}
不過 IDE 會提示你把它移除,因為 Kotlin 會自動型別推斷。
await()
?async()
與 launch()
一樣,都是立刻被排程來執行,如果沒有使用 await()
,在執行這段程式時,也會執行 async()
。
await()
拿掉:fun main() = runBlocking{
async {
println("async start")
delay(500)
println("async end")
return@async 50
}
println("done")
}
→ 執行順序就會跟使用 launch
一樣。
前面我們看了兩個 coroutine builder : launch()
、 async()
,我們知道當程式執行到這邊的時候,就會將這兩個 builder 所建造出來的 coroutine 排進執行的行程中。所以它們預設是立刻就被呼叫的。
有的,我們只需要在使用 launch()
、 async()
時帶入 CoroutineStart.LAZY
即可。
如下:
fun main() = runBlocking {
launch(start = CoroutineStart.LAZY) {
println("launch Start")
delay(500)
println("launch End")
}
println("Done")
}
→ 加上 CoroutineStart.LAZY 之後, launch()
裏面的任務就不會立刻執行了。不過,如果沒有啟動 launch()
那麼程式就會在這邊一直等它執行。
job.start()
來主動啟動 Coroutine 的執行。fun main() = runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
println("launch Start")
delay(500)
println("launch End")
}
job.start()
println("Done")
}
→ 這邊我們也可以使用 job.join()
來啟動。
與 launch()
相同,我們也可以替 async()
加上 CoroutineStart.LAZY 來讓 Coroutine 延後啟動。
async()
可以使用 await()
、 start()
或是 join()
來啟動。
其中, await()
是有包含回傳值得,其他兩個沒有。
函式有分有回傳值的以及沒有回傳值的, 當然 suspend 函式也有,Coroutine 提供了兩種 Coroutine Builder 來處理這兩種不同的 suspend 函式,沒有回傳值的對應的是 launch()
Builder,而有回傳值的對應的是 async()
。雖然這兩個 Coroutine builder 回傳的值不一樣, launch()
回傳的是 Job
,而 async()
回傳的是 Deferred
。但是其實這兩種回傳值都本質上都是一樣的,都是一個可以取消的背景任務。
Job
與 Deferred
共同的函式有 cancel()
、 start()
、 join()
。
其中 cancel()
用來取消 coroutine, start()
用來啟動 coroutine,而 join()
則是會讓 coroutine 的任務完成之後,才把後面的工作加入。
因為 Deferred
是包含回傳值的,所以我們可以使用 await()
來取得 coroutine scope 的回傳值。
最後最後, launch()
以及 async()
都是在執行後立刻會被排進執行的順序。如果想要延後才執行,就要在使用這兩個函式的時候帶入 CoroutineStart.LAZY。
Kotlin Taiwan User Group
Kotlin 讀書會
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局